本系列文章主要分析 JsBridge 框架的原理,学习 H5 和原生 WebView 的交互方式,框架选自 GitHub 上的很火的 H5 + WebView 三方库:lzyzsd/JsBridge,作者是大鬼头;
1 调用接口
在 android 中,我们通过如下方式,使用 jsBridge 框架来和 H5 通信:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| User user = new User(); Location location = new Location(); location.address = "SDU"; user.location = location; user.name = "大头鬼";
webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() { @Override public void onCallBack(String data) { } });
webView.send("hello");
|
这里涉及到 2 个交互接口:
- webView.callHandler:有回调;
- webView.send:没有回调;
下面我会来分析下这两个方法的调用链,这会涉及到 jsBridge 中其他的类:
1 2 3 4 5 6 7 8
| |____Message.java |____WebViewJavascriptBridge.java |____DefaultHandler.java |____BridgeWebView.java |____BridgeWebViewClient.java |____CallBackFunction.java |____BridgeHandler.java |____BridgeUtil.java
|
在分析交互流程的时候,我们会详细分析每个类的内部逻辑!
2 BridgeWebView
我们从 BridgeWebView 开始,先看看内部的一些重要成员变量:
1 2 3 4 5 6 7 8
| Map<String, CallBackFunction> responseCallbacks = new HashMap<String, CallBackFunction>();
Map<String, BridgeHandler> messageHandlers = new HashMap<String, BridgeHandler>();
BridgeHandler defaultHandler = new DefaultHandler();
private List<Message> startupMessage = new ArrayList<Message>();
|
不多说了。
2.1 callHandler
我们先分析有回调的接口的交互流程:
1 2 3 4 5 6
| webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() { @Override public void onCallBack(String data) { } });
|
callHandler 方法指定了 js 使用哪个 Handler 处理 native 的消息!
这里指定了 js 用于处理的 handler name 是 “functionInJs”!
1 2 3 4
| public void callHandler(String handlerName, String data, CallBackFunction callBack) { doSend(handlerName, data, callBack); }
|
我们去看看 doSend 方法:
2.2 doSend
继续分析 doSend 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private void doSend(String handlerName, String data, CallBackFunction responseCallback) { Message m = new Message(); if (!TextUtils.isEmpty(data)) { m.setData(data); } if (responseCallback != null) { String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis())); responseCallbacks.put(callbackStr, responseCallback); m.setCallbackId(callbackStr); } if (!TextUtils.isEmpty(handlerName)) { m.setHandlerName(handlerName); } queueMessage(m); }
|
这里我们看到,会创建一个 Message 对象,封装要发给 js 的消息;
同时注意到,native 的回调并没有传递给 js,而是保存在了内部的一个 responseCallbacks 哈希表中;
实际传递给 js 的是 callbackId;
1 2 3
| Message.data Message.callbackId Message.handlerName
|
最后就是把 message 放入到 message list;
2.3 queueMessage
将 message 放入到 message list;
1 2 3 4 5 6 7 8 9
| private void queueMessage(Message m) { if (startupMessage != null) { startupMessage.add(m); } else { dispatchMessage(m); } }
|
可以看到,这里默认是会将 message 添加到 startupMessage 消息列表中,然后 webview 会处理 message list!
那么在哪里会处理呢?
前面我们分析过,在网页加载好后,会出发 BridgeWebViewClient.onPageFinished 方法,就会启动 native 的消息处理循环!
见 【3.1】 节;
2.4 dispatchMessage
native 给 js 发送消息的关键点,参数 message 是一个消息对象!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| void dispatchMessage(Message m) { String messageJson = m.toJson(); messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2"); messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\""); messageJson = messageJson.replaceAll("(?<=[^\\\\])(\')", "\\\\\'"); messageJson = messageJson.replaceAll("%7B", URLEncoder.encode("%7B")); messageJson = messageJson.replaceAll("%7D", URLEncoder.encode("%7D")); messageJson = messageJson.replaceAll("%22", URLEncoder.encode("%22")); String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson); if (Thread.currentThread() == Looper.getMainLooper().getThread()) { 【*5.1】执行 js 代码; this.loadUrl(javascriptCommand); } }
|
BridgeUtil 是一个工具类,里面主要是一些通信协议码,以及一些工具方法,native 和 H5 通信的时候,本质上是执行 js 代码:
1
| final static String JS_HANDLE_MESSAGE_FROM_JAVA = "javascript:WebViewJavascriptBridge._handleMessageFromNative('%s');";
|
可以看到,执行的 js 代码如下:
1
| javascript:WebViewJavascriptBridge._handleMessageFromNative(JsonString of Message);
|
我相信大家知道,这个方法将进入通信协议 js 文件了!
2.5 handlerReturnData
拦截 url 并处理信息
1 2 3 4 5 6 7 8 9 10
| void handlerReturnData(String url) { String functionName = BridgeUtil.getFunctionFromReturnUrl(url); CallBackFunction f = responseCallbacks.get(functionName); String data = BridgeUtil.getDataFromReturnUrl(url); if (f != null) { f.onCallBack(data); responseCallbacks.remove(functionName); return; } }
|
3 BridgeWebViewClient
WebViewClient 是用于处理各种事件的回调。
3.1 onPageFinished
当 H5 页面加载完成后,会 WebViewClient 方法会处罚;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Override public void onPageFinished(WebView view, String url) { super.onPageFinished(view, url); if (BridgeWebView.toLoadJs != null) { BridgeUtil.webViewLoadLocalJs(view, BridgeWebView.toLoadJs); }
if (webView.getStartupMessage() != null) { for (Message m : webView.getStartupMessage()) { webView.dispatchMessage(m); } webView.setStartupMessage(null); }
onCustomPageFinishd(view,url); }
|
看起来最终调用了 webView.dispatchMessage 方法!
3.2 shouldOverrideUrlLoading
我们来看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { try { url = URLDecoder.decode(url, "UTF-8"); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { webView.handlerReturnData(url); return true; } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { webView.flushMessageQueue(); return true;
} ... ... ... }
|
BridgeUtil 定义了如下的 url 前缀规则:
1 2 3 4 5
| final static String YY_OVERRIDE_SCHEMA = "yy://";
final static String YY_RETURN_DATA = YY_OVERRIDE_SCHEMA + "return/";
final static String YY_FETCH_QUEUE = YY_RETURN_DATA + "_fetchQueue/";
|
3.3 flushMessageQueue
核心方法,从 js 的队列里获取要发送给 native 的 message:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
| void flushMessageQueue() { if (Thread.currentThread() == Looper.getMainLooper().getThread()) { loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {
@Override public void onCallBack(String data) { List<Message> list = null; try { list = Message.toArrayList(data); } catch (Exception e) { e.printStackTrace(); return; } if (list == null || list.size() == 0) { return; } for (int i = 0; i < list.size(); i++) { Message m = list.get(i); String responseId = m.getResponseId(); if (!TextUtils.isEmpty(responseId)) { CallBackFunction function = responseCallbacks.get(responseId); String responseData = m.getResponseData(); function.onCallBack(responseData); responseCallbacks.remove(responseId);
} else { CallBackFunction responseFunction = null; final String callbackId = m.getCallbackId(); if (!TextUtils.isEmpty(callbackId)) { responseFunction = new CallBackFunction() { @Override public void onCallBack(String data) { Message responseMsg = new Message(); responseMsg.setResponseId(callbackId); responseMsg.setResponseData(data); queueMessage(responseMsg); } }; } else { responseFunction = new CallBackFunction() { @Override public void onCallBack(String data) { } }; } BridgeHandler handler; if (!TextUtils.isEmpty(m.getHandlerName())) { handler = messageHandlers.get(m.getHandlerName()); } else { handler = defaultHandler; } if (handler != null){ handler.handler(m.getData(), responseFunction); } } } } }); } }
|
下面部分的代码(【4】)),是和 H5 调用 Native 代码相关的,我们不过多关注!
BridgeUtil 定义了指定 js 协议的 _fetchQueue 方法的命令:
1
| final static String JS_FETCH_QUEUE_FROM_JAVA = "javascript:WebViewJavascriptBridge._fetchQueue();";
|
下面去看一下 loadUrl:
3.4 loadUrl
参数 jsUrl 是 javascript:WebViewJavascriptBridge._fetchQueue();
1 2 3 4 5 6
| public void loadUrl(String jsUrl, CallBackFunction returnCallback) { this.loadUrl(jsUrl); responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback); }
|
这里调用了【*7.1】BridgeUtil.parseFunctionName 对 jsUrl 做了处理,以结果作为 key!
处理入下:
javascript:WebViewJavascriptBridge._fetchQueue(); –> _fetchQueue
这样是有好处了,因为 returnCallback 根据协议是可以复用的,所以这里也保存在了 responseCallbacks 中!!
responseCallbacks 之前我们有分析过!此时 responseCallbacks 放入了 2 个 native 的回调!
3.5 handlerReturnData
改方法用于处理 js 返回给 native 的回调数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| void handlerReturnData(String url) { String functionName = BridgeUtil.getFunctionFromReturnUrl(url); CallBackFunction f = responseCallbacks.get(functionName); String data = BridgeUtil.getDataFromReturnUrl(url); if (f != null) { f.onCallBack(data); responseCallbacks.remove(functionName); return; } }
|
这里调用了【*7.2】BridgeUtil.getFunctionFromReturnUrl 对 url 再次做了处理,前面在 【3.3】 flushMessageQueue,我们将另一个解析回调以 _fetchQueue 为 key,保存到了 responseCallbacks 中,这里是触发他的时候了!
4 Message
该对象用于封装 native 和 js 交互的信息:
4.1 属性
我们来看看他的基本属性:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| private String callbackId;
private String responseId;
private String responseData;
private String data;
private String handlerName;
|
不多说了!
4.2 toJson
将 Message 转为 json string!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public String toJson() { JSONObject jsonObject= new JSONObject(); try { jsonObject.put(CALLBACK_ID_STR, getCallbackId()); jsonObject.put(DATA_STR, getData()); jsonObject.put(HANDLER_NAME_STR, getHandlerName()); String data = getResponseData(); if (TextUtils.isEmpty(data)) { jsonObject.put(RESPONSE_DATA_STR, data); } else { jsonObject.put(RESPONSE_DATA_STR, new JSONTokener(data).nextValue()); } jsonObject.put(RESPONSE_DATA_STR, getResponseData()); jsonObject.put(RESPONSE_ID_STR, getResponseId()); return jsonObject.toString(); } catch (JSONException e) { e.printStackTrace(); } return null; }
|
具体的参数我就不说了,很简单!
5 WebViewJavascriptBridge
最后进入了通信协议 js 脚本:
5.1 _handleMessageFromNative
js 代码中会处理 native 发送的 message json:
1 2 3 4 5 6 7 8 9 10
| function _handleMessageFromNative(messageJSON) { console.log(messageJSON); if (receiveMessageQueue) { receiveMessageQueue.push(messageJSON); } _dispatchMessageFromNative(messageJSON); }
|
在第二篇 js 协议中有讲过:
当在动态注入 js 脚本时,会执行 init 方法,那里会将 receiveMessageQueue 置为 null,同时处理已经包含的 native 消息;
所以这里就直接 _dispatchMessageFromNative 了;
5.2 _dispatchMessageFromNative
js 处理 native 层的消息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| function _dispatchMessageFromNative(messageJSON) { setTimeout(function() { var message = JSON.parse(messageJSON); var responseCallback; if (message.responseId) { ... ... ... } else { if (message.callbackId) { var callbackResponseId = message.callbackId; responseCallback = function(responseData) { _doSend({ responseId: callbackResponseId, responseData: responseData }); }; } var handler = WebViewJavascriptBridge._messageHandler; if (message.handlerName) { handler = messageHandlers[message.handlerName]; } try { handler(message.data, responseCallback); } catch (exception) { if (typeof console != 'undefined') { console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception); } } } }); }
|
最后会选择合适的 handler,将 native message 和 js 回调函数交给 handler 处理!
5.3 _doSend
将结果以回调形式发送给 native!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| function _doSend(message, responseCallback) { if (responseCallback) { var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); responseCallbacks[callbackId] = responseCallback; message.callbackId = callbackId; } sendMessageQueue.push(message); messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; }
|
responseCallback 实际上是 js 处理 native 消息的回调函数,这里给 responseCallback 也分配了一个 id,并保存到 responseCallbacks 中!
目的很明显,是为了建立双向通信!
到这里 Message 中的数据发生了变化:
1 2 3
| Message.responseId Message.callbackId Message.responseData
|
这里创建了一个 url:
这个方法会导致 BridgeWebViewClient.shouldOverrideUrlLoading 触发!
5.4 _fetchQueue
从 sendMessageQueue 队列中获取 message,发送给 native:
1 2 3 4 5 6 7 8 9 10 11
| function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; if (messageQueueString !== '[]') { bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString); } }
|
这里生成了一个新的 url:
再次回到了 shouldOverrideUrlLoading:
6 H5 页面初始化 js 脚本
在 jsBridge 框架中,当 js 协议脚本被动态注入到 H5 中时,会触发 H5 页面中的初始化 js 脚本,该脚本会初始化 js 的 handler:
6.1 connectWebViewJavascriptBridge
可以看到,H5 页面注册的 js handler 的名字就是 “functionInJs” 这个和前面 callHandler 相符合了!
1 2 3 4 5 6 7 8 9 10 11 12 13
| connectWebViewJavascriptBridge(function(bridge) { ... ... ... bridge.registerHandler("functionInJs", function(data, responseCallback) { document.getElementById("show").innerHTML = ("data from Java: = " + data); if (responseCallback) { var responseData = "Javascript Says Right back aka!"; responseCallback(responseData); } }); })
|
这个在前面的 js 通信协议中有分析过,不多说了!
7 BridgeUtil
工具类,包含一些解析方法和协议头常量:
7.1 parseFunctionName
从 url 中解析 funtion name:
1 2 3 4 5
| public static String parseFunctionName(String jsUrl){ return jsUrl.replace("javascript:WebViewJavascriptBridge.", "").replaceAll("\\(.*\\);", ""); }
|
该方法是在 js 创建 url,通知 native 有回调消息后调用的!
7.2 getFunctionFromReturnUrl
从 url 中解析 funtion name:
1 2 3 4 5 6 7 8 9 10 11 12 13
|
public static String getFunctionFromReturnUrl(String url) { String temp = url.replace(YY_RETURN_DATA, EMPTY_STR); String[] functionAndData = temp.split(SPLIT_MARK); if(functionAndData.length >= 1){ return functionAndData[0]; } return null; }
|
该方法是在 native 获取到 js 消息后调用的!
7.3 getDataFromReturnUrl
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
public static String getDataFromReturnUrl(String url) { if(url.startsWith(YY_FETCH_QUEUE)) { return url.replace(YY_FETCH_QUEUE, EMPTY_STR); }
String temp = url.replace(YY_RETURN_DATA, EMPTY_STR); String[] functionAndData = temp.split(SPLIT_MARK);
if(functionAndData.length >= 2) { StringBuilder sb = new StringBuilder(); for (int i = 1; i < functionAndData.length; i++) { sb.append(functionAndData[i]); } return sb.toString(); } return null; }
|
该方法是在 native 获取到 js 消息后调用的,并且在【7.2】调用以后才调用!